﻿using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using GitExtensions.Extensibility;
using GitUI;

namespace GitCommands.Logging;

// TODO capture number of input bytes
// TODO capture number of standard output bytes
// TODO capture number of standard error bytes

public sealed class ProcessOperation
{
    private readonly CommandLogEntry _entry;
    private readonly Stopwatch _stopwatch;
    private readonly Action _raiseCommandsChanged;

    internal ProcessOperation(CommandLogEntry entry, Stopwatch stopwatch, Action raiseCommandsChanged)
    {
        _entry = entry;
        _stopwatch = stopwatch;
        _raiseCommandsChanged = raiseCommandsChanged;
    }

    public void LogProcessEnd(int exitCode, string? error = null) => LogProcessEnd(exitCode, error, ex: null);

    public void LogProcessEnd(Exception ex) => LogProcessEnd(exitCode: null, error: null, ex);

    public void SetProcessId(int processId)
    {
        try
        {
            _entry.ProcessId = processId;
            _raiseCommandsChanged();
        }
        catch (Exception)
        {
            // do not disturb the actual operation
        }
    }

    public void NotifyDisposed()
    {
        if (_entry.Duration is null)
        {
            LogProcessEnd(exitCode: null, error: ".NET object disposed", ex: null);
        }
    }

    private void LogProcessEnd(int? exitCode, string? error, Exception? ex)
    {
        try
        {
            _entry.Duration = _stopwatch.Elapsed;
            _entry.ExitCode = exitCode;
            _entry.Error = error;
            _entry.Exception = ex;
            _raiseCommandsChanged();
        }
        catch (Exception)
        {
            // do not disturb the actual operation
        }
    }
}

public sealed partial class CommandLogEntry
{
    [GeneratedRegex(@"((-c +[^ ""]+(""[^""]*"")?)|(--no-optional-locks)) *", RegexOptions.ExplicitCapture)]
    private static partial Regex GitArgumentsWithoutConfigurationRegex();

    public string FileName { get; }
    public string Arguments { get; }
    public string WorkingDir { get; }
    private DateTime StartedAt { get; }
    public bool IsOnMainThread { get; }
    public TimeSpan? Duration { get; internal set; }
    public int? ProcessId { get; set; }
    public int? ExitCode { get; set; }
    public string? Error { get; set; }
    public Exception? Exception { get; set; }
    public StackTrace? CallStack { get; set; }

    internal CommandLogEntry(string fileName, string arguments, string workingDir, DateTime startedAt, bool isOnMainThread)
    {
        FileName = fileName;
        Arguments = arguments;
        WorkingDir = workingDir;
        StartedAt = startedAt;
        IsOnMainThread = isOnMainThread;
    }

    // Log display name for the git command when run through WSL.
    internal const string WslGitLogName = "~git";

    // Log display name for the git command when run natively.
    internal const string NativeGitLogName = "git";

    public string ColumnLine
    {
        get
        {
            // The git command is handled specially as what we are really interested in is the - non configuration - arguments it was run with.
            // So when a git command is run the displayed command is stripped from its path and config arguments and regardless of the actual
            // git executable name the log will display only `NativeGitLogName` alternatively `WslGitLogName` when the git command is run via WSL.
            // All other commands are displayed as is without any special treatment.

            string wslCmd = AppSettings.WslCommand;
            string wslGitCmd = AppSettings.WslGitCommand;
            string gitCmd = AppSettings.GitCommand;

            string duration = Duration is null
                ? "running"
                : $"{((TimeSpan)Duration).TotalMilliseconds:0,0} ms";

            string fileName;
            string arguments;
            if (FileName.StartsWith(wslCmd) && FileName.EndsWith(wslGitCmd))
            {
                // WSL git commands set in `GitModule`
                // E.g. `FileName` = "wsl <wsl args> git", `Arguments` = "<git args>".
                fileName = WslGitLogName;
                arguments = GetGitArgumentsWithoutConfiguration(Arguments);
            }
            else if (FileName.Equals(wslCmd) && Arguments.IndexOf($" {wslGitCmd} ") is int gitIndex && gitIndex >= 0)
            {
                // WSL git commands set in `FormProcess`
                // E.g. `FileName` = "wsl". `Arguments` = "<wsl args> git <git args>".
                //
                // When both `GitModule` and `FormProcess` instantiates `CommandLogEntry` with the same type of information
                // in `fileName` and `arguments` this if branch and the one above can be merged.
                fileName = WslGitLogName;
                const int numSpaces = 2;
                arguments = GetGitArgumentsWithoutConfiguration(Arguments.Substring(gitIndex + wslGitCmd.Length + numSpaces));
            }
            else if (FileName.EndsWith(gitCmd))
            {
                // All natively run git commands
                // E.g. `FileName` = "git", `Arguments` = "<git args>".
                fileName = NativeGitLogName;
                arguments = GetGitArgumentsWithoutConfiguration(Arguments);
            }
            else
            {
                // Anything other than git commands
                fileName = FileName;
                arguments = Arguments;
            }

            string exit = ExitCode is not null ? $"{ExitCode}" : Exception is not null ? "exc" : string.Empty;
            string ex = Exception is not null ? $"  {Exception.GetType().Name}: {Exception.Message}" : string.Empty;

            return $"{StartedAt:HH:mm:ss.fff} {duration,9} {ProcessId,7} {(IsOnMainThread ? "UI" : "  ")} {exit,3} {fileName} {arguments}{ex}";
        }
    }

    public string CommandLine
    {
        get
        {
            bool quoteFileName = FileName.Contains(' ') && !FileName.StartsWith('"') && !FileName.StartsWith("wsl ");
            return quoteFileName
                ? $"\"{FileName}\" {Arguments}"
                : $"{FileName} {Arguments}";
        }
    }

    public string FullLine(string sep)
    {
        string duration = Duration is null ? "" : $"{((TimeSpan)Duration).TotalMilliseconds:0}";

        string fileName = FileName;

        if (fileName.EndsWith("git.exe"))
        {
            fileName = "git";
        }

        string exit = ExitCode is not null ? $"{ExitCode}" : Exception is not null ? "exc" : string.Empty;
        string ex = Exception is not null ? $"{Environment.NewLine}{Exception}" : string.Empty;
        string callStack = CallStack is not null ? $"{Environment.NewLine}{CallStack}" : string.Empty;

        return $"{StartedAt:O}{sep}{duration}{sep}{ProcessId}{sep}{(IsOnMainThread ? "UI" : "")}{sep}{exit}{sep}{fileName}{sep}{Arguments}{sep}{WorkingDir}{callStack}{ex}";
    }

    public string Detail
    {
        get
        {
            StringBuilder s = new();

            s.Append("File name:   ").AppendLine(FileName);
            s.Append("Arguments:   ").AppendLine(Arguments);
            s.Append("Working dir: ").AppendLine(WorkingDir);
            s.Append("Process ID:  ").AppendLine(ProcessId is not null ? $"{ProcessId}" : "unknown");
            s.Append("Started at:  ").AppendLine($"{StartedAt:O}");
            s.Append("UI Thread?:  ").AppendLine($"{IsOnMainThread}");
            s.Append("Duration:    ").AppendLine(Duration is not null ? $"{Duration.Value.TotalMilliseconds:0.###} ms" : "still running");
            s.Append("Exit code:   ").AppendLine(ExitCode is int exitCode ? ExecutionResult.FormatExitCode(exitCode) : Exception is not null ? $"{Exception}" : "unknown");
            s.Append("Error:       ").AppendLine(Error is not null ? Error : "not captured");
            s.Append("Call stack:  ").Append(CallStack is not null ? $"{Environment.NewLine}{CallStack}" : "not captured");

            return s.ToString();
        }
    }

    public static string GetGitArgumentsWithoutConfiguration(string arguments)
        => GitArgumentsWithoutConfigurationRegex().Replace(arguments, "").TrimEnd();
}

public static class CommandLog
{
    public static event Action? CommandsChanged;

    private static ConcurrentQueue<CommandLogEntry> _queue = new();

    public static IEnumerable<CommandLogEntry> Commands => _queue;

    public static ProcessOperation LogProcessStart(string fileName, string arguments = "", string workDir = "")
    {
        const int MaxEntryCount = 500;

        CommandLogEntry entry = new(fileName, arguments, workDir, DateTime.Now, ThreadHelper.JoinableTaskContext.IsOnMainThread);

        if (AppSettings.LogCaptureCallStacks)
        {
            entry.CallStack = new StackTrace();
        }

        _queue.Enqueue(entry);

        // Trim extra items
        while (_queue.Count >= MaxEntryCount)
        {
            _queue.TryDequeue(out _);
        }

        CommandsChanged?.Invoke();

        return new ProcessOperation(entry, Stopwatch.StartNew(), () => CommandsChanged?.Invoke());
    }

    public static void Clear()
    {
        _queue = new ConcurrentQueue<CommandLogEntry>();
        CommandsChanged?.Invoke();
    }
}
